package br.ugf.projeto.agentes;

import br.ugf.projeto.util.MessageConstants;
import br.ugf.projeto.util.Produto;
import br.ugf.projeto.util.PropertiesHandler;
import br.ugf.projeto.util.Proposta;
import jade.core.AID;
import jade.core.Agent;
import jade.core.behaviours.Behaviour;
import jade.core.behaviours.CyclicBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;

import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * </p>
 * 
 * @author Rodrigo Souza
 * @version 1.0 Created on Mar 28, 2008
 */
public abstract class FornecedorConsumidor
	extends Agent
{

	/**
	 * <p>
	 * </p>
	 * 
	 * @author Rodrigo Souza, Alexandre Zulu, Fabio Girotto
	 * @version 1.0 Created on 01/06/2008
	 */
	protected class IniciaCFP
		extends Behaviour
	{

		/**
		 * <p>
		 * Field <code>serialVersionUID</code>
		 * </p>
		 */
		private static final long serialVersionUID = 1L;

		/**
		 * <p>
		 * </p>
		 * 
		 * @param produto
		 * @param qtde
		 */
		public IniciaCFP( final String produto, final int qtde )
		{
			this.step = 0;
			this.repliesCnt = 0;
			this.produtoNome = produto;
			this.quantidadeProduto = qtde;
			this.fornecedores = procuraFornecedores( produto );
			System.out.printf( "%s INICIA CFP, %s, %d\n", getLocalName(), produto, qtde );
		}

		/**
		 * <p>
		 * </p>
		 * 
		 * @see jade.core.behaviours.Behaviour#action()
		 */
		@Override
		public void action()
		{
			try
			{
				switch ( this.step )
				{
					case 0 :
						// Send the cfp to all sellers
						ACLMessage cfp = new ACLMessage( ACLMessage.CFP );
						for ( int i = 0; i < this.fornecedores.length; ++i )
						{
							cfp.addReceiver( this.fornecedores[i] );
						}
						cfp.addUserDefinedParameter( MessageConstants.PARAMETER_PRODUTO, this.produtoNome );
						cfp.addUserDefinedParameter( MessageConstants.PARAMETER_QTDE, String
							.valueOf( this.quantidadeProduto ) );
						cfp.setContent( MessageConstants.CONTENT_PROPOSAL );
						cfp.setConversationId( MessageConstants.COVERSATION_ID );
						cfp.setReplyWith( "cfp" + System.currentTimeMillis() ); // Unique value
						this.myAgent.send( cfp );
						System.out.printf( "%s ENVIA CFP %s\n", getLocalName(), cfp );
						// Prepare the template to get proposals
						this.template = MessageTemplate.and( MessageTemplate
							.MatchConversationId( MessageConstants.COVERSATION_ID ), MessageTemplate
							.MatchInReplyTo( cfp.getReplyWith() ) );
						this.step = 1;
						break;
					case 1 :
						// Receive all proposals/refusals from seller agents and look for the lowest
						// price
						ACLMessage reply = this.myAgent.blockingReceive( this.template, 30000 );
						this.repliesCnt++;
						if ( this.repliesCnt >= this.fornecedores.length )
						{
							this.step = 2;// We received all replies
						}
						if ( reply != null )
						{
							// Reply received
							if ( reply.getPerformative() == ACLMessage.PROPOSE )
							{
								// This is an offer
								Proposta newOffer = new Proposta(
									reply.getUserDefinedParameter( MessageConstants.PARAMETER_PRODUTO ),
									reply.getSender(),
									Float.parseFloat( reply.getContent() ),
									Integer.parseInt( reply.getUserDefinedParameter( MessageConstants.PARAMETER_QTDE ) ) );
								this.listaPropostas.add( newOffer );
								System.out.printf( "%s RECEBE PROPOSTA %s\n", getLocalName(), reply );
							}
						}
						break;
					case 2 :
						// Sort the seller agent list accordingly to their price offers
						Collections.sort( this.listaPropostas );
						try
						{
							Proposta propostaAceita = this.listaPropostas.get( 0 );
							ACLMessage order = new ACLMessage( ACLMessage.ACCEPT_PROPOSAL );
							order.setContent( MessageConstants.CONTENT_PROPOSAL );
							order.addReceiver( propostaAceita.getFornecedor() );
							order.addUserDefinedParameter( MessageConstants.PARAMETER_PRODUTO, propostaAceita
								.getProduto() );
							order.addUserDefinedParameter( MessageConstants.PARAMETER_VALOR, String
								.valueOf( propostaAceita.getValor() ) );
							order.addUserDefinedParameter( MessageConstants.PARAMETER_QTDE, String
								.valueOf( propostaAceita.getQuantidade() ) );
							order.setConversationId( MessageConstants.COVERSATION_ID );
							order.setReplyWith( MessageConstants.REPLY_WITH_ORDER );
							this.myAgent.send( order );
							this.template = MessageTemplate.and( MessageTemplate
								.MatchConversationId( MessageConstants.COVERSATION_ID ), MessageTemplate
								.MatchInReplyTo( order.getReplyWith() ) );
							System.out.printf( "%s ENVIA ACEPT_PROPOSAL %s\n", getLocalName(), order );
							this.step = 3;
						}
						catch ( RuntimeException e )
						{
							this.step = 0;
						}
						break;
					case 3 :
						// Receive the purchase order reply
						reply = this.myAgent.blockingReceive( this.template, 30000 );
						if ( reply != null )
						{
							// Purchase order reply received
							if ( reply.getPerformative() == ACLMessage.INFORM )
							{
								System.out.printf(
									"%s PRODUTO RECEBIDO %s - Quantidade %s\n",
									this.myAgent.getLocalName(),
									reply.getUserDefinedParameter( MessageConstants.PARAMETER_PRODUTO ),
									reply.getUserDefinedParameter( MessageConstants.PARAMETER_QTDE ) );
							}
						}
						this.step = 4;
						break;
				} // End of switch
			}
			catch ( Exception e )
			{
				e.printStackTrace();
			}
		}

		/**
		 * <p>
		 * </p>
		 * 
		 * @return
		 * @see jade.core.behaviours.Behaviour#done()
		 */
		@Override
		public boolean done()
		{
			return ( this.step == 4 );
		}

		/**
		 * <p>
		 * Field <code>fornecedores</code>
		 * </p>
		 */
		private AID[] fornecedores;

		/**
		 * <p>
		 * Field <code>listaPropostas</code>
		 * </p>
		 */
		private List<Proposta> listaPropostas = new LinkedList<Proposta>();

		/**
		 * <p>
		 * Field <code>produtoNome</code>
		 * </p>
		 */
		private String produtoNome;

		/**
		 * <p>
		 * Field <code>quantidadeProduto</code>
		 * </p>
		 */
		private int quantidadeProduto;

		/**
		 * <p>
		 * Field <code>repliesCnt</code>
		 * </p>
		 */
		private int repliesCnt;

		/**
		 * <p>
		 * Field <code>step</code>
		 * </p>
		 */
		private short step;

		/**
		 * <p>
		 * Field <code>template</code>
		 * </p>
		 */
		private MessageTemplate template;

	}

	/**
	 * <p>
	 * </p>
	 * 
	 * @author Rodrigo Souza, Alexandre Zulu, Fabio Girotto
	 * @version 1.0 Created on 01/06/2008
	 */
	class ServidorOrdensDeCompra
		extends CyclicBehaviour
	{

		/**
		 * <p>
		 * Field <code>serialVersionUID</code>
		 * </p>
		 */
		private static final long serialVersionUID = 1L;

		/**
		 * <p>
		 * </p>
		 * 
		 * @see jade.core.behaviours.Behaviour#action()
		 */
		@Override
		public void action()
		{
			// Process only ACCEPT_PROPOSAL Message
			final MessageTemplate mt = MessageTemplate.MatchPerformative( ACLMessage.ACCEPT_PROPOSAL );
			final ACLMessage msg = this.myAgent.receive( mt );
			if ( msg != null )
			{
				final String msgType = msg.getContent();
				final ACLMessage reply = msg.createReply();
				final Double qtdeD = Double
					.parseDouble( msg.getUserDefinedParameter( MessageConstants.PARAMETER_QTDE ) );
				final int qtde = qtdeD.intValue();
				if ( msgType.compareTo( "Proposal" ) == 0 )
				{
					reply.setPerformative( ACLMessage.INFORM );
					reply.setContent( MessageConstants.CONTENT_INFORM );
					// reply.setConversationId( Constants.COVERSATION_ID );
					this.myAgent.send( reply );
					System.out.printf( "%s ENVIA INFORM_OK %s\n", getLocalName(), reply );
					// // TODO
					// compraRealizada( Produto.PRODUTO_PNEU, ( qtde * 4 ) );
					// compraRealizada( Produto.PRODUTO_VIDROS, ( qtde ) );
					// compraRealizada( Produto.PRODUTO_CARROCERIA, ( qtde ) );
					// // TODO mais componentes
				}
			}
			else
			{
				block();
			}
		}
	}

	/**
	 * <p>
	 * </p>
	 * 
	 * @author Rodrigo Souza, Alexandre Zulu, Fabio Girotto
	 * @version 1.0 Created on 01/06/2008
	 */
	class ServidorPropostas
		extends CyclicBehaviour
	{

		/**
		 * <p>
		 * Field <code>serialVersionUID</code>
		 * </p>
		 */
		private static final long serialVersionUID = 1L;

		/**
		 * <p>
		 * </p>
		 * 
		 * @see jade.core.behaviours.Behaviour#action()
		 */
		@Override
		public void action()
		{
			// Process only CFP Message
			final MessageTemplate mt = MessageTemplate.MatchPerformative( ACLMessage.CFP );
			final ACLMessage msg = this.myAgent.receive( mt );
			try
			{
				if ( msg != null )
				{
					System.out.printf( "%s RECEBE CFP %s\n", getLocalName(), msg );
					final String msgType = msg.getContent();
					final ACLMessage reply = msg.createReply();
					final String produtoNome = msg.getUserDefinedParameter( MessageConstants.PARAMETER_PRODUTO );
					final int qtdePedido = Integer.parseInt( msg
						.getUserDefinedParameter( MessageConstants.PARAMETER_QTDE ) );
					final Integer qtde = FornecedorConsumidor.this.estoqueQtde.get( produtoNome );
					if ( qtde != null )
					{
						if ( msgType.compareTo( MessageConstants.CONTENT_PROPOSAL ) == 0 )
						{
							reply.setPerformative( ACLMessage.PROPOSE );
							reply.setContent( String.valueOf( qtde ) );
						}
						compra( qtdePedido );
					}
					else
					{
						reply.setPerformative( ACLMessage.REFUSE );
						reply.setContent( MessageConstants.CONTENT_NOT_AVAIABLE );
					}
					this.myAgent.send( reply );
					System.out.printf( "%s ENVIA PROPOSTA %s\n", getLocalName(), reply );
				}
				else
				{
					block();
				}
			}
			catch ( final Exception e )
			{
				e.printStackTrace();
			}
		}

		/**
		 * <p>
		 * </p>
		 * 
		 * @param qtdePedido
		 */
		private void compra( final int qtdePedido )
		{
			// Configura o arquivo de propriedades
			PropertiesHandler.setResourceBundle( "config" );
			String produto = Produto.PRODUTO_CARRO_CELTA;
			int qtdeEstoque = FornecedorConsumidor.this.estoqueQtde.get( produto );
			int qtdeEstoqueMinimo = Integer.parseInt( PropertiesHandler.getString( "MONTADORA.ESTOQUE_MINIMO.CELTA" ) );
			System.out.printf( "%s ESTOQUE Produto %s, Quantidade %d\n", getLocalName(), produto, qtdeEstoque );
			// Se existir qtde suficiente em estoque dá baixa, caso contário faz um pedido com a
			// qtde que falta
			if ( ( qtdePedido <= qtdeEstoque ) && ( ( qtdeEstoque - qtdePedido ) >= qtdeEstoqueMinimo ) )
			{
				baixaEstoque( produto, qtdePedido );
			}
			else
			{
				compraRealizada( Produto.PRODUTO_PNEU, ( qtdePedido * 4 ) );
				compraRealizada( Produto.PRODUTO_VIDROS, ( qtdePedido ) );
				compraRealizada( Produto.PRODUTO_CARROCERIA, ( qtdePedido ) );
			}
		}
	}

	/**
	 * <p>
	 * </p>
	 * 
	 * @param produto
	 * @param qtde
	 */
	void baixaEstoque( final String produto, final int qtde )
	{
		int qtdeEstoque = this.estoqueQtde.get( produto );
		this.estoqueQtde.put( produto, ( qtdeEstoque - qtde ) );
	}

	/**
	 * <p>
	 * </p>
	 * 
	 * @param produto
	 * @param qtde
	 */
	public void compraRealizada( final String produto, final int qtde )
	{
		System.out.printf(
			"%s PRODUTO SOLICITADO - Produto %s, Quantidade %d\n",
			getAID().getLocalName(),
			produto,
			qtde );
		verificaEstoque( produto, qtde );
	}

	/**
	 * <p>
	 * </p>
	 */
	protected void inicializaFornecedor()
	{
		String nomeFabricante = getAID().getName();
		Object[] args = getArguments();
		if ( ( args != null ) && ( args.length > 0 ) )
		{
			String tipo = ( String ) args[0];
			double valor = Double.parseDouble( ( String ) args[1] );
			int qtde = Integer.parseInt( ( String ) args[2] );
			this.estoqueValor.put( tipo, valor );
			this.estoqueQtde.put( tipo, qtde );
			System.out
				.println( "--------------------------------------------------------------------------------------------------" );
			System.out
				.printf( "%s - Agente iniciado  Produto: %s | Preço inicial: %.2f\n", nomeFabricante, tipo, valor );
			System.out
				.println( "--------------------------------------------------------------------------------------------------" );
			registraServico( tipo );
		}
		else
		{
			System.out.println( nomeFabricante + " - Agente terminado - erro nos parâmetros" );
			// Make the agent terminate immediately
			doDelete();
		}
	}

	/**
	 * <p>
	 * </p>
	 * 
	 * @param tipo
	 * @return
	 */
	protected AID[] procuraFornecedores( final String tipo )
	{
		// Update the list of seller agents
		System.out.println( getAID().getLocalName() + " buscando fornecedores do tipo: " + tipo );
		AID[] fornecedores = null;
		DFAgentDescription template = new DFAgentDescription();
		ServiceDescription sd = new ServiceDescription();
		sd.setType( tipo );
		template.addServices( sd );
		try
		{
			DFAgentDescription[] result;
			result = DFService.search( this, template );
			fornecedores = new AID[result.length];
			for ( int i = 0; i < result.length; ++i )
			{
				fornecedores[i] = result[i].getName();
			}
			System.out.printf(
				"%s encontrou %d agentes fornecedores de %s\n",
				getAID().getLocalName(),
				result.length,
				tipo );
		}
		catch ( FIPAException fe )
		{
			System.err.println( getAID().getName() + " FALHOU AO BUSCAR " + tipo );
			fe.printStackTrace();
		}
		return fornecedores;
	}

	/**
	 * <p>
	 * </p>
	 * 
	 * @param tipo
	 */
	protected void registraServico( final String tipo )
	{
		// Register the service in the yellow pages
		DFAgentDescription description = new DFAgentDescription();
		description.setName( getAID() );
		ServiceDescription sd = new ServiceDescription();
		sd.setType( tipo );
		sd.setName( getAID().getLocalName() );
		description.addServices( sd );
		try
		{
			System.out.println( getAID().getLocalName()
				+ " registrando-se no servico DF do JADE como fornecedor de: "
				+ tipo );
			DFService.register( this, description );
		}
		catch ( FIPAException e )
		{
			System.err.println( getAID().getName() + " FALHOU AO REGISTRAR-SE NO DF" );
			e.printStackTrace();
		}
	}

	/**
	 * <p>
	 * </p>
	 * 
	 * @see jade.core.Agent#setup()
	 */
	@Override
	protected void setup()
	{
		inicializaFornecedor();
		addBehaviour( new ServidorPropostas() );
		addBehaviour( new ServidorOrdensDeCompra() );
	}

	/**
	 * <p>
	 * </p>
	 * 
	 * @see jade.core.Agent#takeDown()
	 */
	@Override
	protected void takeDown()
	{
		try
		{
			DFService.deregister( this );
		}
		catch ( FIPAException fe )
		{
			fe.printStackTrace();
		}
		finally
		{
			System.err.println( "---------------------------------------" );
			System.err.printf( "Agente %s FINALIZADO\n", getLocalName() );
			System.err.println( "---------------------------------------" );
		}
	}

	/**
	 * <p>
	 * </p>
	 * 
	 * @param produto
	 * @param qtde
	 */
	public void verificaEstoque( final String produto, final int qtde )
	{
		int qtdeEstoque = this.estoqueQtde.get( produto );
		System.out.printf( "%s ESTOQUE Produto %s, Quantidade %d\n", getLocalName(), produto, qtdeEstoque );
		// Se existir qtde suficiente em estoque dá baixa, caso contário faz um pedido com a qtde
		// que falta
		if ( qtde <= qtdeEstoque )
		{
			baixaEstoque( produto, qtde );
		}
		else
		{
			addBehaviour( new IniciaCFP( produto, ( qtde - qtdeEstoque ) ) );
		}
	}

	/**
	 * <p>
	 * Field <code>estoqueQtde</code>
	 * </p>
	 */
	protected Map<String, Integer> estoqueQtde = new HashMap<String, Integer>();

	/**
	 * <p>
	 * Field <code>estoqueValor</code>
	 * </p>
	 */
	protected Map<String, Double> estoqueValor = new HashMap<String, Double>();
}
